﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Text;
using Firecracker.Bootstrapper.Interception.Intercepts;
using Firecracker.Bootstrapper.Utils;
using Mono.Cecil;
using Mono.Cecil.Cil;

namespace Firecracker.Bootstrapper.Interception.Application
{
  [Export(typeof(IInjector))]
  public class VirtualCallInjector : IInjector
  {
    [Import]
    public IInterceptLocator Locator { get; set; }

    public bool ShouldApply(InstructionDefinition definition)
    {
      return definition.Instruction.OpCode == OpCodes.Callvirt;
    }

    public void Apply(InstructionDefinition instructionDefinition)
    {
      var before = instructionDefinition.Instruction.Previous;
      Console.WriteLine(instructionDefinition.Instruction.Previous.OpCode);
      if (before.OpCode == OpCodes.Ldloc_0 || before.OpCode == OpCodes.Ldstr)
      {
        var replacement = GetReplacementCall(instructionDefinition.Instruction);
        if(replacement != null)
        {
          var replacementMethod = instructionDefinition.CallingMethod.Module.Import(replacement);
          var processor = instructionDefinition.CallingMethod.Body.GetILProcessor();

          var newinstructions = new List<Instruction>
                                        {
                                          processor.Create(OpCodes.Ldloc_0),
                                          processor.Create(OpCodes.Isinst, replacementMethod.DeclaringType),
                                          processor.Create(OpCodes.Ldnull),
                                          processor.Create(OpCodes.Cgt_Un),
                                          processor.Create(OpCodes.Ldc_I4_0),
                                          processor.Create(OpCodes.Ceq),
                                          processor.Create(OpCodes.Brtrue_S, instructionDefinition.Instruction.Previous),
                                          processor.Create(OpCodes.Nop),
                                          processor.Create(OpCodes.Ldloc_0),
                                          processor.Create(OpCodes.Callvirt, replacementMethod),
                                          processor.Create(OpCodes.Br, instructionDefinition.Instruction.Next)
                                        };


          var current = instructionDefinition.Instruction.Previous.Previous;

          foreach (var i in newinstructions)
          {
            i.Offset = current.Offset + 1;
            processor.InsertAfter(current, i);
            current = i;
          }

          current = current.Next;
          while (current != null)
          {
            current.Offset = current.Offset + newinstructions.Count;
            current = current.Next;
          }
        }

      }
    }

    private MethodReference GetReplacementCall(Instruction instruction)
    {
      var interceptionTargets = Locator.Locate();

      if (instruction.Operand is MethodReference)
      {
        var call = instruction.Operand as MethodReference;

        var x =  from t in interceptionTargets
                 where call.Name != ".ctor" &&
                       (call.DeclaringType.FullName == t.Target.FullName ||
                        t.Target.GetInteritanceHierarchy().Any(y => y.FullName == call.DeclaringType.FullName))
                 select t;
        if (x.Count() != 1)
          return null;

        var result = from m in x.Single().Replacement.Methods
                     where m.Name == call.Name &&
                           m.Parameters.Count == call.Parameters.Count
                     select m;
        if(result.Count() == 0)
        {
          throw new NoMethodFoundException(x.Single().Replacement, call);
        }
        return result.First();
      }
      return null;
    }
  }

  public class NoMethodFoundException : Exception
  {
    private TypeDefinition _interceptType;
    private MethodReference _call;

    public NoMethodFoundException(TypeReference reference, MethodReference call)
      : base()
    {
      _interceptType = reference.Resolve();
      _call = call;
    }

    public override string Message
    {
      get
      {
        return "Did not find a method in the Replacing class that matches the intercepting call.\n" +
               "Call: " + _call + "\n" +
               "Possible methods: \n" + InterceptMethods;
      }
    }

    public String InterceptMethods
    {
      get
      {
        var methods = _interceptType.Methods.Where(x => x.Name != ".ctor").ToList();
        if (methods.Count() == 0)
          return "None\n";

        return String.Join("\n", "\t" + methods);
      }
    }
  }
}
